package com.zwcl.common.core.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Slf4j
@Component
public class RedisLockLua implements InitializingBean {

    private static final String LUA_LOCK = "  local key=KEYS[1] "
            + "local expireTime =tonumber(ARGV[2]) "
            + "local value = ARGV[1] "
            + "local result = tonumber(redis.call('setnx',key ,value)) "
            + "if result == 1 then "
            + "  redis.call('pexpire',key,expireTime) "
            + "end "
            + "return result";


    private static final String LUA_UNLOCK = " if redis.pcall('get', KEYS[1]) == ARGV[1] then "
            + " return redis.pcall('del', KEYS[1]) "
            + " else return 0 end";

    /*
     * *
     * 限流
     */
    private static final String LUA_PRECISE_LIMIT = " local timeNow = tonumber(ARGV[3]) "
            + " if tonumber(redis.call('llen',KEYS[1]))>= tonumber(ARGV[1]) then "
            + " local tiemOld = tonumber(redis.call('lpop',KEYS[1])) "
            + " redis.call('rpush',KEYS[1],timeNow) "
            + " if timeNow - tonumber(tiemOld) < tonumber(ARGV[2]) then "
            + "    return 0 "
            + " else "
            + "   return 1 "
            + " end "
            + " return 1"
            + " end "
            + " redis.call('rpush',KEYS[1],timeNow) "
            + " return 1 ";


    private static final String IP_RATE_LIMIT = " local key = KEYS[1] "
            + " local limit = tonumber(ARGV[1]) "
            + " local expireTime = ARGV[2] "
            + " local is_exists = redis.call('EXISTS', key) "
            + " if is_exists == 1 then "
            + "   if redis.call('INCR', key) > limit then "
            + "     return 0 "
            + "   else "
            + "     return 1 "
            + "   end "
            + " else "
            + "  redis.call('SET', key, 1) "
            + "  redis.call('EXPIRE', key, expireTime) "
            + "  return 1 "
            + " end ";


    private DefaultRedisScript<Long> lockRedisScript;

    private DefaultRedisScript<Long> unlockRedisScript;

    private DefaultRedisScript<Long> limitRedisScript;

    private DefaultRedisScript<Long> ipLimitRedisScript;

    //用Resource解决RedisTemplate多bean冲突的问题
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void afterPropertiesSet() throws Exception {
        lockRedisScript = new DefaultRedisScript<Long>();
        lockRedisScript.setScriptText(LUA_LOCK);
        lockRedisScript.setResultType(Long.class);

        unlockRedisScript = new DefaultRedisScript<Long>();
        unlockRedisScript.setScriptText(LUA_UNLOCK);
        unlockRedisScript.setResultType(Long.class);

        limitRedisScript = new DefaultRedisScript<Long>();
        limitRedisScript.setScriptText(LUA_PRECISE_LIMIT);
        limitRedisScript.setResultType(Long.class);

        ipLimitRedisScript = new DefaultRedisScript<Long>();
        ipLimitRedisScript.setScriptText(IP_RATE_LIMIT);
        ipLimitRedisScript.setResultType(Long.class);

    }


    /**
     * 获取redis分布式锁
     *  使用例子
     *   boolean lock = redisLock.lock(lockKey, value, RedisConfig.DEFAULT_LOCK_EXPIRED_TIME, 0L);
     *   if(!lock){
     *       throw new DuplicateConsumptionException("Duplicate consumption: "+bizId);
     *   }
     **/

    public boolean lock(String key, String owner, long lockExpireTime, long tryLockTimeout) {
        long timestamp = System.currentTimeMillis();
        // 在超时之前，循环尝试拿锁
        while (tryLockTimeout == 0 || ((System.currentTimeMillis() - timestamp) < tryLockTimeout)) {
            Long result = redisTemplate.execute(new RedisCallback<Long>() {
                @Override
                public Long doInRedis(RedisConnection connection) throws DataAccessException {
                    Long evalResult = connection.eval(LUA_LOCK.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), owner.getBytes(), String.valueOf(lockExpireTime).getBytes());
                    log.debug("lock eval lua result [{}] ", evalResult);
                    return evalResult;
                }

            });
            if (result == 1) {
                return true;
            } else {
                try {
                    // 获取锁失败，睡眠50毫秒继续重试（自旋锁）
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        return false;
    }


    /**
     * 释放redis分布式锁
     *
     * @param key   锁名
     * @param owner 锁的拥有者
     **/

    public void unlock(String key, String owner) {

        redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Long evalResult = connection.eval(LUA_UNLOCK.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), owner.getBytes());
                log.debug("unlock eval lua result [{}] ", evalResult);
                return evalResult;
            }
        });

    }


    /**
     * 限制接口在时间范围调用次数
     *
     * @param key
     * @param limit      访问次数
     * @param expireTime 时间范围(单位为毫秒)
     * @return boolean
     */
    @Deprecated
    public boolean limit(String key, long limit, long expireTime) {

        long result = redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Long evalResult = connection.eval(LUA_PRECISE_LIMIT.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), String.valueOf(limit).getBytes(), String.valueOf(expireTime).getBytes(), String.valueOf(System.currentTimeMillis()).getBytes());
                return evalResult;
            }
        });
        return result == 1;
    }


    /**
     * 限制接口在时间范围调用次数
     *
     * @param key
     * @param limit      访问次数
     * @param expireTime 时间范围(单位为毫秒)
     * @return boolean
     */
    public boolean ipLimit(String key, long limit, long expireTime) {

        long result = redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Long evalResult = connection.eval(IP_RATE_LIMIT.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), String.valueOf(limit).getBytes(), String.valueOf(expireTime).getBytes());
                log.debug("ipLimit eval lua result [{}] ", evalResult);
                return evalResult;
            }
        });
        return result == 1;
    }

}